# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import warnings
from hysop.topology.topology import (
Topology,
TopologyState,
TopologyView,
TopologyWarning,
)
from hysop.constants import np, math, Backend, MemoryOrdering
from hysop.constants import HYSOP_ORDER, BoundaryCondition, HYSOP_INTEGER
from hysop.constants import HYSOP_REAL, HYSOP_MPI_REAL, TranspositionState
from hysop.tools.transposition_states import TranspositionState
from hysop.domain.box import Box, BoxView
from hysop.core.mpi import MPI
from hysop.tools.htypes import check_instance, to_tuple, first_not_None
from hysop.tools.parameters import CartesianDiscretization, MPIParams
from hysop.tools.misc import Utils, prod
from hysop.tools.decorators import debug, deprecated
from hysop.tools.numpywrappers import npw
from hysop.tools.string_utils import prepend
from hysop.mesh.cartesian_mesh import CartesianMesh, CartesianMeshView
from hysop.testsenv import __HAS_OPENCL_BACKEND__
[docs]
class CartesianTopologyState(TopologyState):
"""
CartesianTopology topology state.
This is a helper class to qualify CartesianDiscreteField states.
A CartesianTopologyState contains informations about
the way the application should perceive the contained data
(Arrays) in CartesianDiscreteFields.
Those informations include for the current physical
transposition state of the topology and the local meshes
and the memory_order.
Currently the state is shared accross all components of the
CartesianDiscreteField. This is global state for all processes
contained in the linked Cartesian topology.
"""
__slots__ = ("_is_read_only", "_dim", "_axes", "_memory_order", "_task_id")
@debug
def __new__(
cls, dim, task_id, axes=None, memory_order=None, is_read_only=False, **kwds
):
return super().__new__(cls, is_read_only=is_read_only, **kwds)
@debug
def __init__(
self, dim, task_id, axes=None, memory_order=None, is_read_only=False, **kwds
):
"""
Initialize a CartesianState to given parameters.
Parameters
----------
dim: int
Dimensiong of the underlying domain.
axes: tuple of ints, optional
Permutation of the CartesianTopology and the meshes as tuple of ints,
in numpy notations.
memory_order: :class:`hysop.constants.MemoryOrdering`, optional
Either C or Fortran memory order.
is read_only: bool, optional
Whether this topology is read only or not.
kwds: dict
Base class keyword arguments.
"""
super().__init__(is_read_only=is_read_only, **kwds)
self._dim = int(dim)
self._task_id = int(task_id)
self._set_axes(axes)
self._set_memory_order(memory_order)
def _get_axes(self):
"""Return current permutation as a tuple of int."""
return self._axes
def _set_axes(self, axes):
"""Set the current permutation as a tuple of int."""
axes = axes or TranspositionState[self._dim].default_axes()
axes = to_tuple(axes, cast=int)
check_instance(axes, tuple, values=int)
assert set(axes) == set(range(self._dim))
self._axes = axes
def _get_memory_order(self):
"""Return current memory_order as a tuple (one memory_order per axis)."""
return self._memory_order
def _set_memory_order(self, memory_order):
"""Set the current memory_order as a tuple of hysop.constants.memory_order."""
memory_order = first_not_None(memory_order, MemoryOrdering.C_CONTIGUOUS)
if memory_order == "c":
memory_order = MemoryOrdering.C_CONTIGUOUS
elif memory_order == "f":
memory_order = MemoryOrdering.F_CONTIGUOUS
assert memory_order in (
MemoryOrdering.C_CONTIGUOUS,
MemoryOrdering.F_CONTIGUOUS,
), memory_order
self._memory_order = memory_order
def _get_dim(self):
"""Return the dimension of the underlying topology domain."""
return self._dim
def _get_task_id(self):
"""Return the task identifier of the underlying topology domain."""
return self._task_id
def _get_tstate(self):
"""Return the TranspositionState corresponding to current permutation axes."""
return TranspositionState.axes_to_tstate(self._axes)
def __transposed(self, vec, axes):
"""
Compute permutation of input vector of size len(axes) according
to axes permutation.
"""
axes = to_tuple(axes)
if self.memory_order is MemoryOrdering.F_CONTIGUOUS:
axes = axes[::-1]
assert len(axes) > 0
assert set(axes) == set(range(len(axes))), axes
if isinstance(vec, np.ndarray):
if (vec.ndim > 1) and (vec.ndim == len(axes)):
assert vec.size != len(axes), "ambiguous transposition"
return npw.transpose(vec, axes=axes)
else:
assert vec.size == len(axes), f"{vec.size} != {len(axes)}"
res = vec.copy()
res[...] = tuple(vec[i] for i in axes)
return res
else:
assert len(vec) == len(axes)
return type(vec)(vec[i] for i in axes)
[docs]
def transposed(self, vec):
"""Compute permutation of input vector according to current transposition state."""
return self.__transposed(vec, self._axes)
[docs]
def copy(self, axes=None, memory_order=None, is_read_only=None):
"""Return a copy of self, some properties may be alterted in kwds."""
memory_order = first_not_None(memory_order, self.memory_order)
is_read_only = first_not_None(is_read_only, self.is_read_only)
axes = first_not_None(axes, self.axes)
return CartesianTopologyState(
dim=self.dim,
task_id=self.task_id,
axes=axes,
memory_order=memory_order,
is_read_only=is_read_only,
)
[docs]
def short_description(self):
"""Return a short description of this CartesianTopologyState."""
s = "{}[order={}, axes=({}), ro={}, task={}]"
return s.format(
self.full_tag,
self.memory_order,
",".join(str(a) for a in self.axes),
"1" if self.is_read_only else "0",
self.task_id,
)
[docs]
def long_description(self):
"""Return a long description of this CartesianTopologyState."""
s = """{}
*dim: {}
*task_id: {}
*order: {}
*axes: ({})
*tstate: {}
*read only: {}
"""
return s.format(
self.full_tag,
self.dim,
self.task_id,
self.memory_order,
",".join([str(a) for a in self.axes]),
self.tstate,
self.is_read_only,
)
[docs]
def match(self, other, invert=False):
"""Check if this topology state does match the other one."""
if not isinstance(other, CartesianTopologyState):
return NotImplemented
match = super().match(other, invert=False)
match &= self._dim == other._dim
match &= self._task_id == other._task_id
match &= self._axes == other._axes
match &= self._memory_order == other._memory_order
return not match if invert else match
def __hash__(self):
h = super().__hash__()
return (
h
^ hash(self._dim)
^ hash(self._task_id)
^ hash(self._axes)
^ hash(self._memory_order)
)
dim = property(_get_dim)
task_id = property(_get_task_id)
tstate = property(_get_tstate)
axes = property(_get_axes, _set_axes)
memory_order = property(_get_memory_order, _set_memory_order)
[docs]
class CartesianTopologyView(TopologyView):
"""
CartesianTopology topology view.
Basically a CartesianTopology topology with field transposition state taken into account.
"""
__slots__ = ("_mesh_view", "_domain_view", "_topology", "_topology_state")
@debug
def __init__(self, topology_state, topology=None, **kwds):
super().__init__(topology_state=topology_state, topology=topology, **kwds)
[docs]
@debug
def __new__(cls, topology_state, topology=None, **kwds):
"""
Create and initialize a cartesian topology view.
Parameters
----------
topology_state: :class:`~hysop.topology.cartesian_topology.CartesianTopologyState`
State that charaterizes the given view.
topology: :class:`~hysop.topology.topology.CartesianTopology`
Original cartesian topology on which the view is.
kwds: dict
Base class keyword arguments.
"""
check_instance(topology_state, CartesianTopologyState)
check_instance(topology, CartesianTopology, allow_none=True)
obj = super().__new__(
cls, topology_state=topology_state, topology=topology, **kwds
)
check_instance(obj._topology, CartesianTopology)
return obj
def _get_proc_axes(self):
"""Returns state transposition axes."""
return self._topology_state.axes
def _get_proc_memory_order(self):
"""Returns state memory_order."""
return self._topology_state.memory_order
def _proc_transposed(self, vec):
"""Returns transposed vec according to current transposition state."""
return self._topology_state.transposed(vec)
def _distributed_components(self, vec):
"""
Extract distributed components of vector and returns
reduced vector of same type.
"""
rvec = np.asarray(vec)[self._get_is_distributed()]
if isinstance(vec, np.ndarray):
return rvec.copy()
else:
return type(vec)(rvec.tolist())
# ATTRIBUTE GETTERS
def _get_global_resolution(self):
"""Returns global resolution of the discretization (logical grid size)."""
return self._proc_transposed(self._topology._discretization.global_resolution)
def _get_grid_resolution(self):
"""Returns grid resolution of the discretization (effective grid size)."""
return self._proc_transposed(self._topology._discretization.grid_resolution)
def _get_ghosts(self):
"""Returns ghosts of the discretization."""
return self._proc_transposed(self._topology._discretization.ghosts)
def _get_comm(self):
"""Return the communicator of this topology."""
return self._get_cart_comm()
def _get_cart_comm(self):
"""MPI cartesian topology intracommunicator."""
return self._topology._cart_comm
def _get_cart_dim(self):
"""MPI cartesian topology dimension."""
return self._topology._cart_dim
def _get_cart_size(self):
"""MPI cartesian topology size."""
return self._topology._cart_size
def _get_cart_shape(self):
"""MPI cartesian topology shape."""
return self._distributed_components(self._get_proc_shape())
def _get_cart_periods(self):
"""MPI cartesian topology shape."""
return self._distributed_components(self._get_is_periodic())
def _get_cart_coords(self):
"""Current process MPI cartesian topology coordinates."""
return self._distributed_components(self._get_proc_coords())
def _get_cart_rank(self):
"""Current process MPI cartesian topology rank."""
return self._topology._cart_rank
def _get_cart_ranks(self):
"""Return all MPI cartesian topology ranks as np.ndarray."""
return self._get_proc_ranks().reshape(self._get_cart_shape())
def _get_cart_ranks_mapping(self):
"""Return all parent communicator topology ranks as np.ndarray."""
return self._get_proc_ranks_mapping().reshape(self._get_cart_shape())
def _get_cart_neighbour_ranks(self):
"""Return the ranks of the neighbours nodes as obtained by MPI_Cart_shift.
self.neighbours[0,i] (resp. [1,i]) is the previous (resp. next)
neighbour in axe i."""
return self._get_proc_neighbour_ranks()[:, self._get_is_distributed()]
def _get_proc_shape(self):
"""MPI cartesian topology extended shape (ie. undistributed axes included)."""
return self._proc_transposed(self._topology._proc_shape)
def _get_proc_coords(self):
"""
Current process cartesian topology extended coordinates (ie. undistributed
axes included).
"""
return self._proc_transposed(self._topology._proc_coords)
def _get_proc_ranks(self):
"""
Return all MPI cartesian topology ranks as np.ndarray
undistributed axes included.
"""
return np.transpose(self._topology._proc_ranks, axes=self._get_proc_axes())
def _get_proc_ranks_mapping(self):
"""
Return all parent communicator topology ranks as np.ndarray,
undistributed axes included.
"""
return np.transpose(
self._topology._proc_ranks_mapping, axes=self._get_proc_axes()
)
def _get_proc_neighbour_ranks(self):
"""
Return the ranks of the neighbours nodes as obtained by MPI_Cart_shift.
self.neighbours[0,i] (resp. [1,i]) is the previous (resp. next) neighbour in axe i.
If axe is not distributed, self.neighbours[:,i] returns [-1,-1].
"""
if self._topology_state.memory_order is MemoryOrdering.F_CONTIGUOUS:
prev, next = self._topology._proc_neighbour_ranks[
:, tuple(self._get_proc_axes())
]
return npw.asintegerarray((prev[::-1], next[::-1]))
else:
return self._topology._proc_neighbour_ranks[:, tuple(self._get_proc_axes())]
def _get_is_distributed(self):
"""
Returns Directions which have been distributed,
is_distributed[dir] = True means that data has been distributed along dir.
"""
return self._proc_transposed(self._topology._is_distributed)
def _get_is_periodic(self):
r"""
MPI communicator grid periodicity.
is_periodic[dir] = True means that the MPI grid is periodic along dir.
/!\ This is not equivalent to domain periodicity, as a periodic
direction might not be distributed in the MPI cartesian grid
or might be forced to be periodic for other reasons through
the is_periodic parameter override.
"""
return self._proc_transposed(self._topology._is_periodic)
def _get_distributed_axes(self):
"""Return distributed axes ids as a np.ndarray of integers."""
return np.where(self._get_is_distributed() == True)[0].astype(np.int32)
def _get_periodic_axes(self):
"""Return cartesian communicator periodic axes ids as a np.ndarray of integers."""
return np.where(self._get_is_periodic() == True)[0].astype(np.int32)
global_resolution = property(_get_global_resolution)
grid_resolution = property(_get_grid_resolution)
ghosts = property(_get_ghosts)
comm = property(_get_comm)
cart_comm = property(_get_cart_comm)
cart_dim = property(_get_cart_dim)
cart_size = property(_get_cart_size)
cart_rank = property(_get_cart_rank)
cart_coords = property(_get_cart_coords)
cart_shape = property(_get_cart_shape)
cart_periods = property(_get_cart_periods)
cart_ranks = property(_get_cart_ranks)
cart_ranks_mapping = property(_get_cart_ranks_mapping)
cart_neighbour_ranks = property(_get_cart_neighbour_ranks)
proc_coords = property(_get_proc_coords)
proc_shape = property(_get_proc_shape)
proc_ranks = property(_get_proc_ranks)
proc_ranks_mapping = property(_get_proc_ranks_mapping)
proc_neighbour_ranks = property(_get_proc_neighbour_ranks)
is_distributed = property(_get_is_distributed)
is_periodic = property(_get_is_periodic)
distributed_axes = property(_get_distributed_axes)
periodic_axes = property(_get_periodic_axes)
[docs]
def default_state(self):
"""Return the default topology state of this topology."""
return CartesianTopologyState(
dim=self.domain.dim, task_id=self.mpi_params.task_id
)
[docs]
def short_description(self):
"""
Returns a short description of the current TopologyView.
Short version of long_description().
"""
s = "{}[domain={}, backend={}, pshape={}, "
s += "grid_resolution={}, ghosts={}, bc=({})]"
s = s.format(
self.full_tag,
self.domain.domain.full_tag,
self.backend.kind,
self.proc_shape,
"[{}]".format(",".join(str(s) for s in self.grid_resolution)),
"[{}]".format(",".join(str(g) for g in self.ghosts)),
",".join(
"{}/{}".format(
str(lb).replace("HOMOGENEOUS_", "")[:3],
str(rb).replace("HOMOGENEOUS_", "")[:3],
)
for (lb, rb) in zip(*self.mesh.global_boundaries)
),
)
return s
[docs]
def long_description(self):
"""
Returns a long description of the current TopologyView.
Long version of short_description().
"""
s = f"======== {self.full_tag} ========\n"
s += " *on task: " + str(self.task_id) + "\n"
s += " *backend: " + str(self.backend.full_tag) + "\n"
s += " *shape: " + str(self.proc_shape) + "\n"
s += " *process of coords " + str(self.proc_coords[:])
s += " and of ranks cart_rank={}, parent_rank={}\n".format(
self.cart_rank, self.mpi_params.rank
)
s += " *cartesian ranks map:\n"
s += prepend(str(self.cart_ranks), " " * 4) + "\n"
s += " *cartesian to parent comm ranks mapping:\n"
s += prepend(str(self.proc_ranks_mapping), " " * 4) + "\n"
s += " *neighbours ranks (left, right) x direction \n"
s += prepend(str(self.proc_neighbour_ranks), " " * 4) + "\n"
s += prepend("*" + str(self.domain), " " * 2)
s += prepend("*" + str(self.mesh), " " * 2)
s += "===================================\n"
return s
[docs]
def can_communicate_with(self, target):
"""True if current topo is complient with target.
Complient means :
- all processes in current are in target
- both topologies belong to the same mpi task
"""
if self.topology == target.topology:
return True
msg = "You try to connect topologies belonging to"
msg += " two different mpi tasks. Set taskids properly or use"
msg += " InterBridge."
assert self.task_id == target.task_id, msg
return self.is_consistent_with(target)
[docs]
def is_consistent_with(self, target):
"""
True if target and current object are equal and
have the same parent. Equal means same mesh, same shape and
same domain.
"""
check_instance(target, CartesianTopologyView)
same_parent = self.parent == target.parent
return npw.equal(self.proc_shape, target.proc_shape).all() and same_parent
[docs]
def __str__(self):
"""Same as self.long_description()."""
return self.long_description()
[docs]
class CartesianTopology(CartesianTopologyView, Topology):
"""
CartesianTopology topologies defined on cartesian meshes which communicates
accross processes through a MPI CartesianTopology communicator.
"""
@debug
def __init__(
self,
domain,
discretization,
mpi_params=None,
cart_dim=None,
cart_shape=None,
is_periodic=None,
cutdirs=None,
mesh=None,
cartesian_topology=None,
cl_env=None,
**kwds,
):
super().__init__(
mpi_params=mpi_params,
domain=domain,
discretization=discretization,
cart_dim=cart_dim,
cart_size=None,
proc_shape=None,
is_periodic=is_periodic,
is_distributed=None,
cartesian_topology=id(cartesian_topology),
mesh=hash(mesh),
topology_state=None,
cl_env=cl_env,
**kwds,
)
[docs]
@debug
def __new__(
cls,
domain,
discretization,
mpi_params=None,
cart_dim=None,
cart_shape=None,
is_periodic=None,
cutdirs=None,
mesh=None,
cartesian_topology=None,
cl_env=None,
**kwds,
):
r"""
Initializes or get an existing CartesianTopology topology.
Parameters
----------
domain : :class:`~hysop.domain.box.Box`
The box geometry on which the cartesian topology is defined.
discretization: :class:`~hysop.tools.parameters.CartesianDiscretization`
Description of the global space discretization of the box (resolution, ghosts,
and boundary conditions).
mpi_params : :class:`~hysop.tools.parameters.MPIParams`, optional
MPI parameters (comm, task ...).
If not specified, comm = domain.task_comm, task = domain.curent_task()
backend: :class:`~hysop.constants.Backend` or `~hysop.core.arrays.ArrayBackend`, optional
Backend or backend kind for this topology.
By default a topology will use Backend.HOST.
cart_dim: int, optional
MPI topology dimension.
cart_shape: list or array of int, optional
MPI grid layout, should be sized as the domain dimension.
is_periodic : tuple, list or array of bool, optional
MPI grid periodicity, *overrides* discretization boundary conditions.
cutdirs: list or array of bool, optional
Set which directions may be distributed,
cutdirs[dir] = True allow MPI to distribute data along dir.
mesh: :class:`~hysop.domain.mesh.CartesianMesh`, optional
A predifined mesh (it includes local and global grids.)
cartesian_topology: :class:`~hysop.core.mpi.MPI.Cartcomm`, optional
A predefined mpi cartesian topology. Use this
when you need the same communicator for two
different meshes/space distribution of data.
kwds: dict
Base class arguments.
Includes allocator, cl_env and queue.
Attributes
----------
global_resolution: np.ndarray of HYSOP_INTEGER
Resolution of the global mesh (as given in the discretization parameter).
ghosts: np.ndarray of HYSOP_INTEGER
CartesianDiscretization ghosts of local-to-process mesh (as given in
the discretization parameter).
mesh: :class:`~hysop.domain.mesh.CartesianMeshView`:
Local mesh on the current mpi process.
cart_comm: :class:`~hysop.core.mpi.MPI.Cartcomm`
MPI cartesian topology intracommunicator.
cart_dim: int
MPI_Cart topology dimension.
Dimension of the MPI cartesian communicator.
This dimension may be smaller than the domain dimension, because
some directions may not be distributed.
cart_size: int
MPI_Cart grid size.
Size of the MPI cartesian communicator (total number of MPI processes).
cart_rank: int
MPI_Cart rank.
Rank of the current process in the cartesian communicator.
May be different than self.mpi_params.rank.
cart_coords: np.ndarray of np.int32
Coordinate of this process in the cartesian communicator grid.
The returned tuple is of dimension self.dimension.
cart_shape: np.ndarray of np.int32
MPI_Cart grid shape.
Shape of the MPI cartesian communicator.
cart_periods: np.ndarray of bool
MPI_Cart grid periodicity
cart_ranks: np.ndarray of np.int32
Return all ranks of this cartesian topology as a np.ndarray such
that array[cart_coords] = rank.
cart_ranks_mapping: np.ndarray of np.int32
Return all ranks of the parent MPI communicator as a np.ndarray such
that array[cart_coords] = parent rank.
cart_neighbour_ranks: np.ndarray
Return the ranks of the neighbours nodes as obtained by MPI_Cart_shift.
self.neighbours[0,i] (resp. [1,i]) is the previous (resp. next)
neighbour in direction i.
proc_coords: tuple of int
Coordinates of this process in the extended cartesian grid
(ie. with non distributed directions included).
The returned tuple is of dimension self.domain_dim.
proc_shape: tuple of int
Processus grid shape, same as cart_shape but extended with non distributed
directions.
proc_ranks: np.ndarray of np.int32
Return all ranks of this cartesian topology as a np.ndarray such
that array[proc_coords] = rank.
proc_ranks_mapping: np.ndarray of np.int32
Return all ranks of the parent MPI communicator as a np.ndarray such
that array[proc_coords] = parent rank.
proc_neighbour_ranks: np.ndarray
Return the ranks of the neighbours nodes as obtained by MPI_Cart_shift.
self.neighbours[0,i] (resp. [1,i]) is the previous (resp. next)
neighbour in axe i.
If axe is not distributed, self.neighbours[:,i] returns [-1,-1].
is_distributed : tuple of bool
Directions which have been distributed,
is_distributed[dir] = True means that data has been distributed along dir.
is_periodic : tuple of bool
MPI grid periodicity.
is_periodic[dir] = True means that the MPI grid is periodic along dir.
/!\ This is not equivalent to domain periodicity, as a periodic
direction might not be distributed in the MPI cartesian grid
or might be forced to be periodic for other reasons through
the is_periodic parameter override.
Domain periodicity is self.domain.periodicity
Notes:
------
* Almost all parameters above are optional.
Only one must be chosen among dim, cutdirs and shape.
See :ref:`topologies` in the Hysop User Guide for details.
* When cartesian_topology is given, dim, shape and cutdirs parameters,
if set, are not used to build the mpi topology, but compared with
cartesian_topology parameters. If they do not fit, error is raised.
* Unless is_periodic is specified periodicity is extracted
from domain boundary conditions.
* All attributes are read-only properties.
"""
# Get or create mpi parameters
mpi_params = cls._create_mpi_params(mpi_params, domain, cl_env)
# prepare MPI processes layout
(cart_dim, cart_size, proc_shape, is_periodic, is_distributed) = (
cls._check_topo_parameters(
mpi_params,
domain,
discretization,
cart_shape,
cutdirs,
cart_dim,
is_periodic,
cartesian_topology,
)
)
# double check types, to be sure RegisteredObject will work as expected
check_instance(mpi_params, MPIParams)
check_instance(domain, Box)
check_instance(discretization, CartesianDiscretization)
check_instance(cart_dim, (int, np.integer))
check_instance(cart_size, (int, np.integer))
check_instance(proc_shape, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(is_periodic, np.ndarray, dtype=bool)
check_instance(is_distributed, np.ndarray, dtype=bool)
check_instance(cartesian_topology, MPI.Cartcomm, allow_none=True)
check_instance(mesh, CartesianMesh, allow_none=True)
cart_dim = int(cart_dim)
cart_size = int(cart_size)
npw.set_readonly(proc_shape, is_periodic, is_distributed)
topology_state = CartesianTopologyState(
dim=domain.dim, task_id=mpi_params.task_id
)
obj = super().__new__(
cls,
mpi_params=mpi_params,
domain=domain,
discretization=discretization,
cart_dim=cart_dim,
cart_size=cart_size,
proc_shape=proc_shape,
is_periodic=is_periodic,
is_distributed=is_distributed,
cartesian_topology=id(cartesian_topology),
mesh=hash(mesh),
topology_state=topology_state,
cl_env=cl_env,
**kwds,
)
if not obj.obj_initialized:
obj.__initialize(
domain,
discretization,
cart_dim,
cart_size,
proc_shape,
is_periodic,
is_distributed,
cartesian_topology,
mesh,
)
return obj
def __initialize(
self,
domain,
discretization,
cart_dim,
cart_size,
proc_shape,
is_periodic,
is_distributed,
cartesian_topology,
mesh,
):
self._discretization = discretization
self._cart_dim = cart_dim
self._cart_size = cart_size
self._proc_shape = proc_shape
self._is_periodic = is_periodic
self._is_distributed = is_distributed
if cartesian_topology is None:
cartesian_topology = self._build_mpi_topo()
self._set_topo(cartesian_topology)
# Get features of the mpi processes grid
self._extract_topo_features(domain)
# Computation of the local meshes
if mesh is None:
mesh = self._compute_mesh(domain, discretization)
self._mesh = mesh
self._TopologyView__set_mesh(mesh)
npw.set_readonly(
self._proc_coords,
self._proc_shape,
self._proc_ranks,
self._proc_neighbour_ranks,
self._is_distributed,
self._is_periodic,
)
if __debug__:
self.__check_vars()
def __check_vars(self):
# check variables and properties at the same time
check_instance(self.global_resolution, np.ndarray, HYSOP_INTEGER)
check_instance(self.ghosts, np.ndarray, HYSOP_INTEGER)
check_instance(self.cart_comm, MPI.Cartcomm)
check_instance(self.cart_dim, int, minval=1)
check_instance(self.cart_size, int, minval=1, maxval=self.mpi_params.size)
check_instance(self.cart_rank, int, minval=0, maxval=self._cart_size)
check_instance(self.cart_coords, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.cart_shape, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.cart_ranks, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.cart_neighbour_ranks, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.cart_ranks_mapping, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.cart_periods, np.ndarray, dtype=bool)
check_instance(self.proc_coords, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.proc_shape, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.proc_ranks, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.proc_neighbour_ranks, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.proc_ranks_mapping, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.distributed_axes, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.periodic_axes, np.ndarray, dtype=HYSOP_INTEGER)
check_instance(self.is_distributed, np.ndarray, dtype=bool)
check_instance(self.is_periodic, np.ndarray, dtype=bool)
check_instance(self.domain, BoxView)
check_instance(self.mesh, CartesianMeshView)
[docs]
def topology_like(
self,
backend=None,
grid_resolution=None,
ghosts=None,
lboundaries=None,
rboundaries=None,
mpi_params=None,
cart_shape=None,
**kwds,
):
"""Return a topology like this object, possibly altered."""
assert "global_resolution" not in kwds, "Specify grid_resolution instead."
grid_resolution = first_not_None(
grid_resolution, self._discretization.grid_resolution
)
ghosts = first_not_None(ghosts, self._discretization.ghosts)
lboundaries = first_not_None(lboundaries, self._discretization.lboundaries)
rboundaries = first_not_None(rboundaries, self._discretization.rboundaries)
backend = first_not_None(backend, self._backend)
discretization = CartesianDiscretization(
resolution=grid_resolution,
ghosts=ghosts,
lboundaries=lboundaries,
rboundaries=rboundaries,
)
# find out the target mpi_params
if __HAS_OPENCL_BACKEND__:
from hysop.core.arrays.all import OpenClArrayBackend
if isinstance(backend, OpenClArrayBackend):
if (mpi_params is not None) and (
mpi_params != backend.cl_env.mpi_params
):
msg = "Backend mpi params mismatch."
raise RuntimeError(msg)
mpi_params = backend.cl_env.mpi_params
mpi_params = first_not_None(mpi_params, self.mpi_params)
if self.domain.dim == grid_resolution.size:
cart_shape = first_not_None(cart_shape, self.proc_shape)
return CartesianTopology(
domain=self._domain,
mpi_params=mpi_params,
discretization=discretization,
backend=backend,
cart_shape=self.proc_shape,
cartesian_topology=None,
**kwds,
)
@classmethod
def _check_topo_parameters(
cls,
mpi_params,
domain,
discretization,
shape,
cutdirs,
dim,
is_periodic,
cartesian_topology,
):
"""
Check compatibility between input parameters provided
to build the mpi topology and return shape of the cartesian topology.
The layout can be obtained by:
(a) - from an already built cartesian topology
(b) - from 'shape' parameter : we choose explicitely the layout.
(c) - from 'cutdirs' parameter: we choose the directions
to split and let MPI fix the number of processes in each dir.
(d) - from dimension of the topology ==> let MPI
choose the 'best' layout.
(e) - in last resort the dimension will be the dimension of the domain.
"""
domain_dim = domain.dim
parent_size = mpi_params.comm.Get_size()
if cartesian_topology:
msg = "Wrong type for input communicator."
assert isinstance(cartesian_topology, MPI.Cartcomm), msg
msg = "The given cartesian topology does not fit with the others"
msg += " input parameters."
assert (dim is None) or (cartesian_topology.dim == dim), msg
assert (shape is None) or (cartesian_topology.size == prod(shape)), msg
assert (shape is None) or (
shape == npw.asintegerarray(cartesian_topology.dims)
).all(), msg
assert is_periodic is not None
dim = cartesian_topology.dim
cart_shape = cartesian_topology.dims
shape = npw.dim_ones(domain_dim)
shape[:dim] = cart_shape
elif shape is not None:
# method (b)
msg = " parameter is useless when shape is provided."
assert cutdirs is None, "cutdirs " + msg
assert dim is None, "dim " + msg
dim = domain_dim
shape = npw.asintegerarray(shape)
msg = "Input shape must be of "
msg += "the same size as the domain dimension."
assert shape.size == dim, msg
elif cutdirs is not None:
# method (c)
msg = " parameter is useless when cutdirs is provided."
assert shape is None, "shape " + msg
assert dim is None, "dim " + msg
shape = npw.dim_ones(domain_dim)
is_distributed = npw.asboolarray(cutdirs).copy()
if is_distributed.any():
dim = np.sum(is_distributed > 0)
assert dim <= domain_dim, "cutdirs is not of size domain_dim"
cart_shape = npw.asintegerarray(MPI.Compute_dims(parent_size, dim))
cls._optimize_shape(cart_shape)
assert (
sum(cutdirs) == cart_shape.size
), "Created shape {} doesnt respect specified cutdirs {}".format(
np.sum(cutdirs > 0), cart_shape.size
)
shape[is_distributed > 0] = cart_shape
else:
assert parent_size == 1
else:
if dim is not None:
# method (d)
msg = " parameter is useless when dim is provided."
assert shape is None, "shape " + msg
assert cutdirs is None, "cutdirs " + msg
else:
# method (e)
dim = domain_dim
cart_shape = npw.asintegerarray(MPI.Compute_dims(parent_size, dim))
shape = npw.dim_ones(domain_dim)
shape[:dim] = cart_shape
cls._optimize_shape(shape)
if is_periodic is None:
try:
is_periodic = discretization.periodicity
except AttributeError:
msg = "Given CartesianDiscretization was not setup correctly:"
msg += "\n => Boundary conditions have not been set."
msg += "\n{}"
msg = msg.format(discretization)
raise ValueError(msg)
shape = npw.asintegerarray(shape)
is_periodic = np.asarray(is_periodic) != 0
assert shape.size == domain_dim
assert is_periodic.size == domain_dim
is_distributed = shape > 1
if parent_size == 1:
is_distributed[-1] = True
cart_shape = shape[is_distributed]
cart_dim = cart_shape.size
cart_size = prod(cart_shape)
is_periodic = is_periodic * is_distributed
assert (cart_dim > 0) and (cart_dim <= domain_dim)
assert prod(cart_shape) == prod(shape)
assert (cart_shape <= shape[is_distributed]).all()
assert cart_size <= parent_size
assert is_periodic.size == domain_dim
assert is_distributed.size == domain_dim
if cart_size < parent_size:
msg = "Only {} processes will be used in the CartesianTopology topology "
msg += "(parent communicator contains {} processes)."
msg = msg.format(cart_size, parent_size)
warnings.warn(msg, TopologyWarning)
proc_shape = shape
return (cart_dim, cart_size, proc_shape, is_periodic, is_distributed)
def _build_mpi_topo(self):
cart_shape = self._proc_shape[self._is_distributed]
periods = self._is_periodic[self._is_distributed]
return self.mpi_params.comm.Create_cart(
cart_shape, periods=periods, reorder=True
)
def _set_topo(self, cartesian_topology):
"""Check and set self._cart_comm and self._cart_rank."""
msg = "Wrong type for input communicator."
assert isinstance(cartesian_topology, MPI.Cartcomm), msg
comm = cartesian_topology
assert self.cart_dim == comm.ndim
assert self.cart_size == comm.Get_size()
assert (
self._proc_shape[self._is_distributed] == npw.asintegerarray(comm.dims)
).all()
assert (
self._is_periodic[self._is_distributed] == npw.asboolarray(comm.periods)
).all()
self._cart_comm = comm
self._cart_rank = comm.Get_rank()
def _extract_topo_features(self, domain):
"""Set self._proc_coords, self._proc_ranks, self._proc_ranks_mapping."""
is_distributed = self._is_distributed
proc_shape = self._proc_shape
cart_comm = self._cart_comm
mpi_params = self._mpi_params
domain_dim = domain.dim
cart_shape = proc_shape[is_distributed]
cart_coords = npw.asintegerarray(cart_comm.coords)
proc_coords = npw.dim_zeros(domain_dim)
proc_coords[is_distributed] = cart_coords
proc_ranks = npw.dim_zeros(proc_shape)
cart_ranks = proc_ranks.reshape(cart_shape)
proc_ranks_mapping = npw.dim_zeros(proc_shape)
cart_ranks_mapping = proc_ranks_mapping.reshape(proc_ranks_mapping.size)
proc_neighbour_ranks = npw.dim_zeros(shape=(2, domain_dim))
for coords in np.ndindex(*cart_shape):
rank = cart_comm.Get_cart_rank(coords)
cart_ranks[coords] = rank
cart_ranks_mapping[...] = MPI.Group.Translate_ranks(
cart_comm.group, cart_ranks.flatten(), mpi_params.comm.group
)
direction = 0
for i in range(self.domain_dim):
if is_distributed[i]:
proc_neighbour_ranks[:, i] = self._cart_comm.Shift(direction, 1)
direction += 1
else:
proc_neighbour_ranks[:, i] = (-1, -1)
assert direction == self.cart_dim
self._proc_coords = npw.asintegerarray(proc_coords)
self._proc_ranks = npw.asintegerarray(proc_ranks)
self._proc_ranks_mapping = npw.asintegerarray(proc_ranks_mapping)
self._proc_neighbour_ranks = npw.asintegerarray(proc_neighbour_ranks)
@staticmethod
def _optimize_shape(shape):
"""
Reorder shape according to the data layout
if arrays are in "fortran" order (column major)
the last dir has priority for distribution.
For C-like (row major) arrays, first dir is the
first to be distributed, and so on.
"""
shape.sort()
if HYSOP_ORDER == "C":
shape[:] = shape[::-1]
def _compute_mesh(self, domain, discretization):
assert isinstance(domain, Box)
assert isinstance(discretization, CartesianDiscretization)
assert (
self.domain_dim == discretization.grid_resolution.size
), "The resolution size differs from the domain dimension."
proc_coords = self._proc_coords
proc_shape = self._proc_shape
# Find out dimension and periodic axes of the domain
domain_dim = domain.dim
periodicity = discretization.periodicity
# /!\ Now we assume that the user gives us the grid resolutionn
# and not the global_resolution as it used to be.
# We do not remove 1 point on each periodic axe because of periodicity
computational_grid_resolution = discretization.grid_resolution
# Number of "computed" points (i.e. excluding ghosts).
# /!\ we try to match fftw_mpi_local_size_* functions for the Fortran spectral backend
assert all(computational_grid_resolution >= proc_shape)
pts_noghost = npw.dim_zeros(domain_dim)
pts_noghost[:] = npw.ceil(
npw.divide(computational_grid_resolution.astype(HYSOP_REAL), proc_shape)
)
# If any, remaining points are added on the mesh of the last process.
remaining_points = npw.dim_zeros(domain_dim)
remaining_points[:] = (
computational_grid_resolution - (proc_shape - 1) * pts_noghost
)
assert (remaining_points >= 1).all(), remaining_points
# Total number of points (size of arrays to be allocated)
nbpoints = pts_noghost.copy()
for i in range(domain_dim):
if proc_coords[i] == proc_shape[i] - 1:
nbpoints[i] = remaining_points[i]
local_resolution = nbpoints.copy()
local_resolution += 2 * discretization.ghosts
msg = "\nLocal compute shape is smaller than the total number of ghosts, "
msg += "on at least one axis:"
msg += f"\n *compute shape: {nbpoints}"
msg += f"\n *ghosts: 2*{discretization.ghosts}"
if not np.all(nbpoints >= 2 * discretization.ghosts):
raise RuntimeError(msg)
# Global indices for the local mesh points
global_start = proc_coords * pts_noghost
return CartesianMesh(
topology=self, local_resolution=local_resolution, global_start=global_start
)
[docs]
def view(self, topology_state):
"""
Returns a view of this topology with the given state.
"""
check_instance(topology_state, CartesianTopologyState)
return CartesianTopologyView(topology=self, topology_state=topology_state)
[docs]
def discretize(self, field):
"""Discretize a continous field on this topology and return a DiscreteField."""
from hysop.fields.continuous_field import ScalarField
from hysop.fields.cartesian_discrete_field import (
CartesianDiscreteScalarField,
TmpCartesianDiscreteScalarField,
)
check_instance(field, ScalarField)
if (field.lboundaries_kind != self._discretization.lboundaries).any() or (
field.rboundaries_kind != self._discretization.rboundaries
).any():
msg = """
Cannot discretize a field with cartesian boundary conditions:'
lboundaries: {}
rboundaries: {}
On a cartesian topology with different boundary conditions:
lboundaries: {}
rboundaries: {}
""".format(
field.lboundaries_kind,
field.rboundaries_kind,
self._discretization.lboundaries,
self._discretization.rboundaries,
)
raise RuntimeError(msg)
if field.is_tmp:
return TmpCartesianDiscreteScalarField(field=field, topology=self)
else:
return CartesianDiscreteScalarField(field=field, topology=self)